package joq.server;

import joq.*;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

public class SocketServer
{
    private MessageQueue queue;
    private Selector select;
    private ServerSocketChannel server;
    private Charset charSet;

    public SocketServer(int port) throws IOException
    {
	charSet = Charset.forName("ISO-8859-1");
	queue = new MessageQueue();
	select = Selector.open();
	server = ServerSocketChannel.open();
	server.configureBlocking(false);
	server.socket().bind(new InetSocketAddress(port));
	server.register(select, SelectionKey.OP_ACCEPT);
    }

    /**
     * Runs the listener loop.
     */
    public void listen()
    {
	while (true)
	    try {
		operate();
	    } catch (IOException e) {
		e.printStackTrace();
	    }
    }

    /**
     * Selects available sockets and acts on them.
     * @throws IOException
     */
    private void operate() throws IOException
    {
	// Block for connection
	try {
	    select.select();
	} catch (ClosedSelectorException e) {
	    // Possibly shut down system? Restart server? Attempt restart n times?
	    Logger.getLogger("joq").log(Level.INFO, "Socket was closed.");
	    return;
	} catch (IOException e) {
	    Logger.getLogger("joq").log(Level.WARNING, "Socket error: " + e);
	    return;
	}

	Set keys = select.selectedKeys();
	Iterator it = keys.iterator();

	while (it.hasNext()) {
	    SelectionKey key = (SelectionKey)it.next();
	    it.remove();

	    // Accept new connections
	    if (key.isAcceptable()) {
		SocketChannel client = server.accept();
		client.configureBlocking(false);
		client.register(select, SelectionKey.OP_READ, new Transaction(charSet, queue));
		continue;
	    }

	    /*
	     * Read client: this may not be the entire request, so do a
	     * buffered read into the attached Request object, which will
	     * be decoded when the key comes up as writable.
	     */
	    if (key.isReadable()) {
		SocketChannel client = (SocketChannel)key.channel();
		Transaction tr = (Transaction)key.attachment();
		try {
		    if (tr.receive(client)) { // request complete
			if (tr.getStatus() == Transaction.STATUS.OPEN) {
			    key.interestOps(SelectionKey.OP_WRITE);
			    // Deal with request
			} else {
			    Logger.getLogger("joq").log(Level.INFO, "[" + client.socket().getInetAddress().toString() + "] unexpectedly closed connection");
			    key.cancel();
			    client.close();
			}
		    }
		} catch (IOException e) {
		    Logger.getLogger("joq").log(Level.WARNING, "Dropping connection [" + client.socket().getInetAddress().toString() + "] due to exception:");
		    e.printStackTrace();
		    key.cancel();
		    client.close();
		} catch (ProtocolError e) {
		    Logger.getLogger("joq").log(Level.WARNING, "Protocol error [" + client.socket().getInetAddress().toString() + "]");
		    e.printStackTrace();
		    key.cancel();
		    client.close();
		}
		continue;
	    }

	    /*
	     * Write client: entire response may not be written at once.
	     */
	    if (key.isWritable()) {
		// Write responses
		SocketChannel client = (SocketChannel)key.channel();
		Transaction tr = (Transaction)key.attachment();
		if (tr.getStatus() == Transaction.STATUS.OPEN)
		    tr.send(client);
		key.cancel();
		client.close();
		continue;
	    }
	}
    }
}
